const uuid = require("uuid");
const axios = require("axios").default.create({
  headers: {
    "User-Agent": "okhttp/3.10.0"
  }
});
const util = require("util");
const path = require("path");
const fs = require("fs");
const buffer = require("buffer");
const url = require("url");
const crypto = require("crypto");
const qs = require("querystring")
const FormData = require("form-data");
const randomstring = require("randomstring");
const random_number = require("random-number");
const rannum = random_number;
const ProgressBar = require("progress");
const randomcolor = require("randomcolor");
const JsB64 = require("js-base64").Base64;
const PromiseQueue = require("promise-queue")
const runParallel = require("run-parallel-limit");
const LibOwhat = require("lib-owhat/lib/v0.0.1/");
const Toolbox = require("./Toolbox");
const CommonAxerrHandlerGen = require("./CommonAxerrHandler");
const FileMakers = require("./FileMakers").FileMakers
const TokenGetter = require("./TokenGetter").TokenGetter


class DirFileMaker {
  /**
   * @param {String} site_name
   * @returns {Promise<ModelQiniuDirUploadedInfo}
   * @param {Array<{path:String,data:Array<{ctx:String,chunk_size:Number }> }>} mkblk_ok_files 
   * @param {(file_info:{path:String,data:Array<{ctx:String,chunk_size:Number}>},size_limit:Number,part_name_func:Function)=>Promise<{ok:Boolean,msg:String,data:Array<{filepath:String,link:String}>}>} mkfile_func 
   * @param {Array<{path:String,reason:String}>} mkblk_failed_paths 
   * @param {(full_path:string,parts_count:number,part_index:number)=>string} part_name_func
   * @param {Boolean} is_mfwfile
   */
  static upload_dir(mkblk_ok_files, mkfile_func, site_name, mkblk_failed_paths, part_name_func, is_mfwfile = false) {
    return new Promise(async resolve => {
      /**@type {Array<{path:String,link:string}>} */
      let mkfile_ok_files = [];
      /**@type {Array<{path:String,reason:string}>} */
      let mkfile_failed_files = [];
      for (let blk_file of mkblk_ok_files) {
        // console.log(site_name, "mkfile for [", blk_file.path, "]");
        let o_mkfile = await mkfile_func(blk_file, undefined, part_name_func);
        /**@todo 这里应该允许多次重试 */
        if (o_mkfile.ok) {
          o_mkfile.data.forEach(d => {
            mkfile_ok_files.push({
              path: d.filepath,
              link: d.link
            });
          });
          await TokenGetter.pause_promise_gen(random_number({ min: 100, max: 1100 }));
        } else {
          mkfile_failed_files.push({
            path: blk_file.path,
            reason: o_mkfile.msg
          })
        }
        console.log("mkfile for [", blk_file.path, "]", site_name, o_mkfile.ok);
      }
      let result_info = new ModelQiniuDirUploadedInfo(mkblk_failed_paths,
        mkfile_failed_files,
        mkfile_ok_files,
        site_name, is_mfwfile);
      resolve(
        result_info
      );
    })
  }
}

class ModelQiniuDirUploadedInfo {

  /**
   * @param {String} site_name 在哪个网站的域名下
   * @param {Array<{path:String,reason:String}>} mkblk_failed_paths 
   * @param {Array<{path:String,reason:String}>} mkfile_failed_paths 
   * @param {Array<{path:String,link:String}>} upload_ok_paths 
   * @param {Boolean} is_mfwfile
   */
  constructor(mkblk_failed_paths, mkfile_failed_paths, upload_ok_paths, site_name, is_mfwfile) {
    this.mkblk_failed_paths = mkblk_failed_paths;
    this.mkfile_failed_paths = mkfile_failed_paths;
    this.upload_ok_paths = upload_ok_paths;
    this.site_name = site_name;
    this.is_mfwfile = is_mfwfile;
    /**@type {()=>string} */
    this.toHTML = () => {
      let ok_color = ($ => {
        let num = parseInt(random_number({ max: 9, min: 0 }));
        let lu = num % 2 ? "light" : "dark";
        let hues = ["#FF0080", "blue", "green", "pink", "purple"];
        let hue_index = parseInt(random_number({ min: 0, max: hues.length - 1 }));
        return randomcolor({ luminosity: lu, hue: hues[hue_index] })
      })()
      let ok_items = this.upload_ok_paths.map(d => `<a href="${d.link}?attname=${is_mfwfile ? encodeURIComponent(path.basename(`${d.path}.mfwfile`)) : encodeURIComponent(path.basename(d.path))}"><font color="${
        ok_color}"><b><u>${d.path}</u></b></font></a>`).join("<br>");
      let mkblk_failed_items = this.mkblk_failed_paths.map(d => `<font color="${randomcolor({ luminosity: "dark", hue: ok_color })}"><mark><del>${d.path}</del></mark> : </font><br><pre>${d.reason}</pre>`).join("<br>");
      let mkfile_failed_items = this.mkfile_failed_paths.map(d => `<font color="${randomcolor({ luminosity: "dark", hue: ok_color })}"><i>${d.path}</i> : </font><br><pre><code>${d.reason}</code></pre>`).join("<br>");
      let error_part1 = mkblk_failed_items ? `<hr color="gray"><h5><font color="${ok_color}">make block failed(${this.site_name}) :</font></h5><blockquote>${mkblk_failed_items}</blockquote>` : "";
      let error_part2 = mkfile_failed_items ? `<hr color="#FF0055"><h5><font color="${ok_color}">make file failed(${this.site_name}) :</font></h5><blockquote>${mkfile_failed_items}</blockquote>` : "";
      let success_part = `<h5><font color="${ok_color}">OK LINKS (${this.site_name}) : </font></h5><blockquote>${ok_items}</blockquote>`;
      return `${error_part1}${error_part2}${success_part}`;
    }

    /**@type {()=>string} */
    this.toBearyChatMarkdown = () => {
      let ok_items = this.upload_ok_paths.map(d => `> [${
        d.path
        }](${d.link}?attname=${encodeURIComponent(path.basename(d.path))})`).join("\n");
      let mkblk_failed_items = this.mkblk_failed_paths.map(d => `~~${d.path}~~:\n\`\`\`\n${d.reason}\n\`\`\`\n`).join("\n***\n");
      let mkfile_failed_items = this.mkfile_failed_paths.map(d => `*${d.path}*:\n\`\`\`\n${d.reason}\n\`\`\`\n`).join("\n***\n");
      let error_part1 = mkblk_failed_items ? `---\n **:cry: make block failed(${
        this.site_name
        }):**\n ${mkblk_failed_items}` : ``;
      let error_part2 = mkfile_failed_items ? `---\n **:scream: mkfile failed(${
        this.site_name
        }):**\n ${mkfile_failed_items}` : '';
      let success_part = `**:smile: OK LINKS(${this.site_name}) :**\n${ok_items}`;
      return `${error_part1}\n${error_part2}${success_part}`

    }
  }
}

const MQDUI = ModelQiniuDirUploadedInfo;


class QiniuUniversally {

  constructor() {
    this.mkfile_queue = new PromiseQueue(1, Infinity);
  }

  /**
   * @private
   * @returns {Promise<{ok:Boolean,msg:String,data:Array<{ctx:String,chunk_size:Number}>}>}>}
   * @param {String} file_fullpath 
   * @param {Number} max_parallel_upload
   * @param {Number} max_retry_for_4MB
   */
  mkblk_4_file(file_fullpath, max_retry_for_4MB = 5, max_parallel_upload = 10) {
    max_retry_for_4MB = parseInt(max_retry_for_4MB);
    if (max_retry_for_4MB <= 1) {
      max_retry_for_4MB = 1;
    }
    return new Promise(async resolve => {
      let o_stats = await Toolbox.getStats(file_fullpath);
      if (!(o_stats.ok && o_stats.stats.isFile())) {
        return resolve({
          ok: false,
          msg: o_stats.ok ? `${file_fullpath} is not a file` : o_stats.msg
        })
      }
      if (o_stats.stats.size == 0) {
        return resolve({
          ok: false,
          msg: `[EMPTY FILE]the size of ${file_fullpath} is ZERO!`
        })
      }
      let o_token = await TokenGetter.get_any();
      if (!o_token.ok) {
        return resolve({
          ok: false,
          msg: `[in mkblk_4_file]failed to get any token:${o_token.msg}`
        })
      }
      const S4MB = 4 * 1024 * 1024;
      const CHUNKS_COUNT = Math.ceil(o_stats.stats.size / S4MB);
      const basename_of_file = `${uuid()}.${path.extname(file_fullpath)}`;
      /**@type {String[]} */
      const ctxs = [];
      /**@type {Number[]} */
      const chunk_sizes = []
      /**@type {Function[]} */
      let tasks = [];
      let bar = new ProgressBar(`[mkblk] ${path.basename(file_fullpath)}[:bar] :percent :etas'`, {
        complete: "#",
        incomplete: "_",
        width: 40,
        total: CHUNKS_COUNT
      });
      for (let i = 0; i < CHUNKS_COUNT; i++) {
        let start = i * S4MB;
        let end = start + S4MB - 1;
        if (end >= o_stats.stats.size) {
          end = o_stats.stats.size - 1;
        }
        let chunk_size = end - start + 1;
        tasks.push(async cb => {
          let headers = {
            'Authorization': `UpToken ${o_token.data.uptoken}`,
            "Content-Type": "application/octet-stream",
            "Content-Length": chunk_size,
            "host": "upload.qiniup.com",
            "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36 OPR/63.0.3368.71"
          };
          let upload_url = `https://${process.env['QINIUP_IP'] || "upload.qiniup.com"}/mkblk/${chunk_size}?name=${encodeURI(basename_of_file)}&chunk=${i}&chunks=${CHUNKS_COUNT}`;
          // console.log("upload url is",upload_url)
          let try_index = 1;
          /**@type {Array<string>} */
          let errs = [];
          while (try_index <= max_retry_for_4MB) {
            let o_try_upchunk = await Toolbox.WrapHttpPostStream(fs.createReadStream(file_fullpath, { start: start, end: end }),
              upload_url, headers);
            // console.log(`mkblk file=[${path.basename(file_fullpath)}],i=${i},retry_i=${try_index},o_up_chunk.ok=${o_try_upchunk.ok},${o_try_upchunk.ok ? "" : o_try_upchunk.msg}`);
            if (o_try_upchunk.ok && o_try_upchunk.json_data && o_try_upchunk.json_data['ctx']) {
              bar.tick(1)
              return cb(null, {
                index: i,
                ctx: o_try_upchunk.json_data['ctx'],
                chunk_size: chunk_size
              })
            } else {
              try_index++;
              errs.push(o_try_upchunk.msg);
              await TokenGetter.pause_promise_gen(try_index * 1000);
            }
          }
          cb({
            ok: false,
            msg: `failed to mkblk after ${max_retry_for_4MB}-times try:${errs.join("---\n---")}`
          })
        });
      };
      runParallel(tasks, 10 || max_parallel_upload, (err, results) => {
        if (err) {
          return CommonAxerrHandlerGen(resolve)(err['msg'] || err['message'] || err)
        }
        results.forEach(r => {
          ctxs[r['index']] = r['ctx'];
          chunk_sizes[r['index']] = r['chunk_size'];
        });
        resolve({
          ok: true,
          msg: "ok",
          data: ctxs.map((ctx, i) => {
            return {
              ctx: ctx,
              chunk_size: chunk_sizes[i]
            }
          })
        })
      })

    })
  }

  /**
   * @api private
   * @returns {Promise<{ok:Boolean,msg:String,failed_paths:Array<{path:String,reason:String}>,ok_files:Array<{path:String,data:Array<{ctx:String,chunk_size:Number }> }>  }>}
   * @param {String} dir_fullpath 
   * @param {Number} max_retry_for_4MB 
   * @param {Number} max_parallel_upload 
   * @param {string} base_dir
   */
  mkblk_4_dir(dir_fullpath, max_retry_for_4MB = 5, max_parallel_upload = 10, base_dir = dir_fullpath) {
    return new Promise(async resolve => {
      console.log(`[${dir_fullpath}] mkblk start....`);
      let o_stats = await Toolbox.getStats(dir_fullpath);
      if (!(o_stats.ok && o_stats.stats.isDirectory())) {
        let error = o_stats.ok ? `${dir_fullpath} is not a dir` : `getStats error:${o_stats.msg}`;
        return resolve({
          ok: false,
          msg: error
        })
      }
      let o_list = await Toolbox.safeListDir(dir_fullpath);
      /**@type {Array<{path:String,data:Array<{ctx:String,chunk_size:Number }> }>} */
      let ok_files = [];
      /**@type {Array<{path:String,reason:String}>} */
      let failed_paths = [];
      for (let item of o_list) {
        if (item.stats.isDirectory()) {
          let o_mkblk_child_dir = await this.mkblk_4_dir(item.full_path, max_retry_for_4MB, max_parallel_upload, base_dir);
          if (o_mkblk_child_dir.ok) {
            ok_files = ok_files.concat(o_mkblk_child_dir.ok_files);
            failed_paths = failed_paths.concat(o_mkblk_child_dir.failed_paths);
          } else {
            failed_paths.push({
              path: path.relative(base_dir, item.full_path),
              reason: o_mkblk_child_dir.msg
            })
          }
          // if (failed_paths.length >= 1) {
          //   debugger
          // }
        } else if (item.stats.isFile()) {
          let o_mkblk4file = await this.mkblk_4_file(item.full_path, max_retry_for_4MB, max_parallel_upload);
          if (o_mkblk4file.ok) {
            ok_files.push({
              path: path.relative(base_dir, item.full_path),
              data: o_mkblk4file.data
            })
          } else {
            failed_paths.push({
              path: path.relative(base_dir, item.full_path),
              reason: o_mkblk4file.msg
            })
          }
        }

      }
      console.log(`[${dir_fullpath}] mkblk end....`);
      resolve({
        ok: true,
        msg: "ok",
        failed_paths: failed_paths,
        ok_files: ok_files
      })
    })
  }


  /**
   * @api public
   * @returns {Promise<{ok:Boolean,msg:String,data:{dúapp:MQDUI,babytree:MQDUI,hiaita:MQDUI,meipian:MQDUI,udesk:MQDUI,owhat:MQDUI,_7moor:MQDUI,pianke:MQDUI,daovoice:MQDUI,hunliji:MQDUI,tuniu:MQDUI}}>}
   * @param {String} file_fullpath 
   * @param {Number} max_retry_for_4MB 
   * @param {(full_path:string,parts_count:number,part_index:number)=>string} part_name_func
   * @param {Number} max_parallel_upload 
   * @param {Boolean} with_meipian 是否上传美篇(这东西可能费时间)
   */
  do_upload_file(file_fullpath, max_retry_for_4MB = 5, max_parallel_upload = 10, part_name_func = FileMakers.default_part_name_func, with_meipian = false) {
    return new Promise(async resolve => {
      this.mkfile_queue.add($ => new Promise(async queue_resolve => {
        let o_mkblk = await this.mkblk_4_file(file_fullpath, max_retry_for_4MB, max_parallel_upload);
        queue_resolve();
        if (!o_mkblk.ok) {
          return resolve({
            ok: false,
            msg: `failed to mkblk for ${file_fullpath}:${o_mkblk.msg}`
          })
        }
        let p_info_owhat = DirFileMaker.upload_dir([{
          path: path.basename(file_fullpath),
          data: o_mkblk.data
        }], FileMakers.mkfile_owhat, "qimage.owhat.cn", [], part_name_func);
        let p_info_7moor = DirFileMaker.upload_dir([{
          path: path.basename(file_fullpath),
          data: o_mkblk.data
        }], FileMakers.mkfile_7moor, "fs-im-kefu.7moor.com", [], part_name_func);
        let p_info_pianke = new Promise(r => r(new MQDUI([], [], [],
          "片刻网即将停止服务 - qnimg.pianke.me"))) || DirFileMaker.upload_dir([{
            path: path.basename(file_fullpath),
            data: o_mkblk.data
          }], FileMakers.mkfile_pianke, "qnimg.pianke.me", [], part_name_func);
        let p_info_udesk = new Promise(r => r(new MQDUI([], [], [],
          "Udesk 他们换了阿里云 - qn-im.udesk.cn"))) || DirFileMaker.upload_dir([{
            path: path.basename(file_fullpath),
            data: o_mkblk.data
          }], FileMakers.mkfile_udesk, "qn-im.udesk.cn", [], part_name_func);
        let p_info_daovoice = new Promise(r => r(new MQDUI([], [], [],
          "Daovoice 他们限制了单文件大小 - daocom-uploads-cdn.daovoice.io"))) || DirFileMaker.upload_dir([{
            path: path.basename(file_fullpath),
            data: o_mkblk.data
          }], FileMakers.mkfile_daovoice, "daocom-uploads-cdn.daovoice.io", [], part_name_func);
        let p_info_hunliji = DirFileMaker.upload_dir([{
          path: path.basename(file_fullpath),
          data: o_mkblk.data
        }], FileMakers.mkfile_hunliji, "qnm.hunliji.com", [], part_name_func);
        let p_info_tuniu = new Promise(r => r(new MQDUI([], [], [],
          "s.tuniu.net 现在只能传图片了?? - s.tuniu.net"))) || DirFileMaker.upload_dir([{
            path: path.basename(file_fullpath),
            data: o_mkblk.data
          }], FileMakers.mkfile_tuniu, "s.tuniu.net", [], part_name_func, true);
        let p_info_meipian = with_meipian ? DirFileMaker.upload_dir([{
          path: path.basename(file_fullpath),
          data: o_mkblk.data
        }], FileMakers.mkfile_meipian, "static2.ivwen.com", [], part_name_func, true) :
          new Promise(r => r(new MQDUI([], [], [],
            "NO-FILES-TO-BE-UPLOADED-TO-static2.ivwen.com")));
        let p_info_hiaita = new Promise(r => r(new MQDUI([], [], [],
          "inyota(hiaita)从token限制了尺寸 pic.hiaita.com"))) || DirFileMaker.upload_dir([{
            path: path.basename(file_fullpath),
            data: o_mkblk.data
          }], FileMakers.mkfile_hiaita, "pic.hiaita.com", [], part_name_func);
        let p_info_babytree = DirFileMaker.upload_dir([{
          path: path.basename(file_fullpath),
          data: o_mkblk.data
        }], FileMakers.mkfile_babytree, "pic10.babytree.com", [], part_name_func);
        let p_info_duapp = DirFileMaker.upload_dir([{
          path: path.basename(file_fullpath),
          data: o_mkblk.data
        }], FileMakers.mkfile_dú, "du.hupucdn.com", [], part_name_func);
        let infos = await Promise.all([
          p_info_7moor,
          p_info_owhat,
          p_info_pianke,
          p_info_daovoice,
          p_info_hunliji,
          p_info_tuniu,
          p_info_udesk,
          p_info_meipian,
          p_info_hiaita,
          p_info_babytree,
          p_info_duapp
        ]);
        resolve({
          ok: true,
          msg: "ok",
          data: {
            _7moor: infos[0],
            owhat: infos[1],
            pianke: infos[2],
            daovoice: infos[3],
            hunliji: infos[4],
            tuniu: infos[5],
            udesk: infos[6],
            meipian: infos[7],
            hiaita: infos[8],
            babytree: infos[9],
            dúapp: infos[10]
          }
        });
      }))
    })
  }

  /**
   * @api public
   * @returns {Promise<{ok:Boolean,msg:String,data:{dúapp:MQDUI,babytree:MQDUI,hiaita:MQDUI,meipian:MQDUI,udesk:MQDUI,owhat:MQDUI,_7moor:MQDUI,pianke:MQDUI,daovoice:MQDUI,hunliji:MQDUI,tuniu:MQDUI}}>}
   * @param {String} dir_fullpath 
   * @param {Number} max_retry_for_4MB 
   * @param {(full_path:string,parts_count:number,part_index:number)=>string} part_name_func
   * @param {Number} max_parallel_upload 
   * @param {Boolean} with_meipian 是否上传美篇(这东西可能费时间)
   */
  do_upload_dir(dir_fullpath, max_retry_for_4MB = 5, max_parallel_upload = 10, part_name_func = FileMakers.default_part_name_func, with_meipian = false) {
    return new Promise(async resolve => {
      this.mkfile_queue.add(() => new Promise(async quque_resolve => {
        let p = this.mkblk_4_dir(dir_fullpath, max_retry_for_4MB, max_parallel_upload);
        p.then($ => {
          console.log("mkblk for dir", dir_fullpath, "done!");
          quque_resolve();
        });
        let o_mkblk = await p;
        if (!o_mkblk.ok) {
          return resolve({
            ok: false,
            msg: `failed to mkblk for dir ${dir_fullpath}:${o_mkblk.msg}`
          })
        }
        let p_info_owhat = DirFileMaker.upload_dir(o_mkblk.ok_files,
          FileMakers.mkfile_owhat, "qimage.owhat.cn", o_mkblk.failed_paths, part_name_func);
        let p_info_7moor = DirFileMaker.upload_dir(o_mkblk.ok_files,
          FileMakers.mkfile_7moor, "fs-im-kefu.7moor.com", o_mkblk.failed_paths, part_name_func);
        let p_info_pianke = new Promise(r => r(new MQDUI([], [], [],
          "片刻网即将停止服务 - qnimg.pianke.me"))) || DirFileMaker.upload_dir(o_mkblk.ok_files,
            FileMakers.mkfile_pianke, "qnimg.pianke.me", o_mkblk.failed_paths, part_name_func);
        let p_info_daovoice = DirFileMaker.upload_dir(o_mkblk.ok_files,
          FileMakers.mkfile_daovoice, "daocom-uploads-cdn.daovoice.io", o_mkblk.failed_paths, part_name_func);
        let p_info_hunliji = DirFileMaker.upload_dir(o_mkblk.ok_files,
          FileMakers.mkfile_hunliji, "qnm.hunliji.com", o_mkblk.failed_paths, part_name_func);
        let p_info_tuniu = DirFileMaker.upload_dir(o_mkblk.ok_files,
          FileMakers.mkfile_tuniu, "s.tuniu.net", o_mkblk.failed_paths, part_name_func, true);
        let p_info_udesk = DirFileMaker.upload_dir(o_mkblk.ok_files,
          FileMakers.mkfile_udesk, "qn-im.udesk.cn", o_mkblk.failed_paths, part_name_func);
        let p_info_meipian = with_meipian ? DirFileMaker.upload_dir(o_mkblk.ok_files,
          FileMakers.mkfile_meipian, "static2.ivwen.com", o_mkblk.failed_paths, part_name_func, true) : new Promise(r => r(new MQDUI([], [], [],
            "NO-FILES-TO-BE-UPLOADED-TO-static2.ivwen.com")));
        let p_info_hiaita = new Promise(r => r(new MQDUI([], [], [],
          "inyota(hiaita)从token限制了尺寸 pic.hiaita.com"))) || DirFileMaker.upload_dir(o_mkblk.ok_files,
            FileMakers.mkfile_hiaita, "pic.hiaita.com", o_mkblk.failed_paths, part_name_func);
        let p_info_babytree = DirFileMaker.upload_dir(o_mkblk.ok_files,
          FileMakers.mkfile_babytree, "pic10.babytree.com", o_mkblk.failed_paths, part_name_func);
        let p_info_duapp = DirFileMaker.upload_dir(o_mkblk.ok_files,
          FileMakers.mkfile_dú, "du.hupucdn.com", o_mkblk.failed_paths, part_name_func);
        let infos = await Promise.all([
          p_info_7moor,
          p_info_owhat,
          p_info_pianke,
          p_info_daovoice,
          p_info_hunliji,
          p_info_tuniu,
          p_info_udesk,
          p_info_meipian,
          p_info_hiaita,
          p_info_babytree,
          p_info_duapp
        ]);
        resolve({
          ok: true,
          msg: "ok",
          data: {
            _7moor: infos[0],
            owhat: infos[1],
            pianke: infos[2],
            daovoice: infos[3],
            hunliji: infos[4],
            tuniu: infos[5],
            udesk: infos[6],
            meipian: infos[7],
            hiaita: infos[8],
            babytree: infos[9],
            dúapp: infos[10]
          }
        })

      }));




    })
  }


}

QiniuUniversally.__debug = {
  FileMakers: FileMakers,
  TokenGetter: TokenGetter
}


module.exports = {
  QiniuUniversally: QiniuUniversally,
  ModelQiniuDirUploadedInfo: ModelQiniuDirUploadedInfo
}